package scalab.core

import java.io.{File, FileFilter}
import scalab.utils.Reporter
import scalab.core.component.{ExecutableComponent, Component,Task, Filter}
import scalab.core.event._
import scalab.core.interpreter.Build
import scalab.core.utils.Lock
import java.io.{FileNotFoundException, IOException}
import scalab.utils.Path
import scalab.core.dependencies.{Dependency, HardDependency}

sealed trait Cache extends Reporter{

	//---------BEGIN EVENTS
	private var runStartedListeners: List[Listener[RunStarted]] = Nil
	private var runFinishedListeners: List[Listener[RunFinished]] = Nil
	private var loadStartedListeners: List[Listener[LoadStarted]] = Nil
	private var loadFinishedListeners: List[Listener[LoadFinished]] = Nil
	private var storeStartedListeners: List[Listener[StoreStarted]] = Nil
	private var storeFinishedListeners: List[Listener[StoreFinished]] = Nil

	def listenRunStarted(listener: RunStarted => Unit){
			runStartedListeners = Listener[RunStarted](listener) :: runStartedListeners
	}
	def listenRunFinished(listener: RunFinished => Unit){
			runFinishedListeners = Listener[RunFinished](listener) :: runFinishedListeners
	}
	def listenLoadStarted(listener: LoadStarted => Unit){
			loadStartedListeners = Listener[LoadStarted](listener) :: loadStartedListeners
	}
	def listenLoadFinished(listener: LoadFinished => Unit){
			loadFinishedListeners = Listener[LoadFinished](listener) :: loadFinishedListeners
	}
	def listenStoreStarted(listener: StoreStarted => Unit){
			storeStartedListeners = Listener[StoreStarted](listener) :: storeStartedListeners
	}
	def listenStoreFinished(listener: StoreFinished => Unit){
			storeFinishedListeners = Listener[StoreFinished](listener) :: storeFinishedListeners
	}

	protected def fireRunStarted(comp: ExecutableComponent){
		val event = RunStarted(comp)
		runStartedListeners foreach {l => l(event)}
	}
	protected def fireRunFinished(comp: ExecutableComponent, res: Boolean){
		val event = RunFinished(comp, res)
		runFinishedListeners foreach {l => l(event)}
	}
	protected def fireLoadStarted(comp: ExecutableComponent){
		val event = LoadStarted(comp)
		loadStartedListeners foreach {l => l(event)}
	}
	protected def fireLoadFinished(comp: ExecutableComponent, ex: Boolean){
		val event = LoadFinished(comp, ex)
		loadFinishedListeners foreach {l => l(event)}
	}
	protected def fireStoreStarted(comp: ExecutableComponent){
		val event = StoreStarted(comp)
		storeStartedListeners foreach {l => l(event)}
	}
	protected def fireStoreFinished(comp: ExecutableComponent, succeeded: Boolean){
		val event = StoreFinished(comp, succeeded)
		storeFinishedListeners foreach {l => l(event)}
	}

	private[core] def reverseListeners{
		runStartedListeners = runStartedListeners.reverse
		runFinishedListeners = runFinishedListeners.reverse
		loadStartedListeners = loadStartedListeners.reverse
		loadFinishedListeners = loadFinishedListeners.reverse
		storeStartedListeners = storeStartedListeners.reverse
		storeFinishedListeners = storeFinishedListeners.reverse
	}

	//shortcuts
	def listenBuildStarted(listener: BuildStarted => Unit): Unit = 
		Build.listenBuildStarted(listener)
	def listenBuildFinished(listener: BuildFinished => Unit): Unit = 
		Build.listenBuildFinished(listener)

	//--------END EVENTS

	Cache.caches = this :: Cache.caches

	protected var cacheDirectory: String = Cache.defaultCacheDir
	final def setCacheDir(dir: String){
		if(dir eq null)
			warning("Cannot set cache directory to null! " +
				"Please use NoCache if you want to disable the cache.")
		else
			cacheDirectory = Path(dir)
	}
	final def getCacheDir: String = cacheDirectory
	
	private var activated: Boolean = true
	final def enabled: Boolean = activated
	final def enable{
		if(enabled)info("Cache is already enabled!")
		else activated = true
	}

	final def disable{
		if(enabled)activated = false
		else info("Cache is already disabled!")
	}

	final def toggleEnabled{
		if(enabled)disable
		else enable
	}

	//protected? to allow only use in cache development
	final def taskDirectory(task: Task): String = 
		 HardDependency(cacheDirectory).canonicalPath + File.separator + task.id + File.separator
	
	final def setAsDefault = Cache.setDefaultCache(this)

	final private[core] def locallyUpToDate(task: Task, products: Set[Dependency]): Boolean = {
		val deps = task.sources//take inputs into account in the hash!
		val (noMiss, runProducts) = getRunProducts(task, deps)
		if(noMiss && !runProducts.isEmpty){
			val hash1 = resourcesHash(products)
			val hash2 = resourcesHash(deps)
			hash1 == hash2
		}
		else
			false
	}

	//CacheCorePolicy
	//def initializeCCP: Unit //do this with events (smarter)
	//initializeCCP
	//def finalizeCCP: Unit //=> CCP can work in memory (faster) and store to peristent storage at commit
	private[core] def run(task: Task): Boolean
	private[core] def run(filter: Filter): Boolean
	protected def freeSpace //runs the CEP
	protected def setTaskProductDir(task: Task, dir: String)
	protected def notifyUsed(productDirectory: File)
	def getCachedProductDirs(taskDir: File): Iterable[File]
	def getAllCachedProductDirs: Iterable[File]
	//boolean indicates whether there was only cache hits
	def getRunProducts(task: Task, sources: Iterable[Dependency]): Pair[Boolean, Option[Set[Dependency]]]

	//Change Detection Policy
	def dependenciesHash(deps: Iterable[Dependency]): String
	def dependencyHash(dep: Dependency): String
	def resourceHash(res: Resource): String
	def resourcesHash(res: Iterable[Resource]): String

	//Eviction Policy
	protected var thresholdSize: Long
	protected var evictedSize: Long
	def setThresholdSize(size: Long): Unit
	def getThresholdSize: Long
	def setEvictedSize(size: Long)
	def getEvictedSize: Long
	//def compareDependencies(d1: Dependency, d2: Dependency): Int
	def evictionOrder(files: Iterable[File]): Stream[File]
	def cacheSize: Long
}

trait CacheCorePolicy extends Cache{
	//override me
	protected def locker(task: Task): String = taskDirectory(task) + "lock" 
	protected def load(task: Task): Boolean
	protected def load(filter: Filter): Boolean
	protected def store(task: Task): Boolean
	protected def store(filter: Filter): Boolean
	final private[core] def run(task: Task): Boolean = {
		val lock = Lock(locker(task))
		//info("acquired lock: " + lock.mutex.canonicalPath)
		fireRunStarted(task)
		var res: Boolean = true
		if(enabled){
			//freeSpace
			fireLoadStarted(task)
			if(load(task)){
				fireLoadFinished(task, true)
				Resource(task.productDirectory).mkdirs
				task.fireExecutionStarted
				res = task.execute(task.productDirectory) 
				task.fireExecutionFinished(res)
				if(res){
					fireStoreStarted(task)
					res = store(task)
					fireStoreFinished(task, res)
				}
			}
			else{ //task does not execute
				fireLoadFinished(task, false)
				res = true
			}
		}
		else{
			task.fireExecutionStarted
			res = task.execute(task.productDirectory)
			task.fireExecutionFinished(res)
		}
		freeSpace
		lock.release
		fireRunFinished(task, res)
		res
	}

	final private[core] def run(filter: Filter): Boolean = {
		if(enabled){
			freeSpace
			if(load(filter)){
				filter.execute && store(filter)
			}
			else
				true
		}
		else{
			filter.execute
		}
	}

	def freeSpace{
		var size = cacheSize
		/*info("Cache size is " + cacheSize + " bytes")
		info("thresholdSize is " + thresholdSize + " bytes")*/
		if(size > thresholdSize){
			var sortedDeps = evictionOrder(getAllCachedProductDirs)
			//info("cachedProductDirs: " + sortedDeps)
			var diff = 1L
			while(!sortedDeps.isEmpty && size > evictedSize /*&& diff > 0L*/){
				diff = free(sortedDeps.head)
				size -= diff
				sortedDeps = sortedDeps.tail
			}
		}
	}

	def notifyUsed(productDirectory: File) = ()

	final def getAllCachedProductDirs: Iterable[File] = {
		val f = new File(cacheDirectory)
		val filter = new FileFilter{
			def accept(file: File): Boolean = file.isDirectory
		}

		f.listFiles(filter) flatMap {dir => getCachedProductDirs(dir)}
	}

	protected def free(file: File): Long

	final def setTaskProductDir(task: Task, dir: String){
		task.productDirectory = dir
	}

	def cacheSize: Long = {
		import scalab.utils.FileUtils
		FileUtils.dirSize(cacheDirectory)
	}
}

trait ChangeDetectionPolicy extends Cache{

	import scalab.utils.Hexadecimal
	import java.security.MessageDigest

	var digestAlgorithm: String = "MD5"

	final def dependenciesHash(deps: Iterable[Dependency]): String = {
		val ordered = Dependency.orderDependencies(deps)
		val md = MessageDigest.getInstance(digestAlgorithm)
		ordered foreach {d => hashDependency(md, d)}
		Hexadecimal.fromByteArray(md.digest)
	}

	final def dependencyHash(dep: Dependency): String = {
		val md = MessageDigest.getInstance(digestAlgorithm)
		hashDependency(md, dep)
		Hexadecimal.fromByteArray(md.digest)
	}

	final def resourcesHash(res: Iterable[Resource]): String = {
		val ordered = Resource.orderResources(res)
		val md = MessageDigest.getInstance(digestAlgorithm)
		ordered foreach {r => hashResource(md, r)}
		Hexadecimal.fromByteArray(md.digest)
	}

	final def resourceHash(res: Resource): String = {
		val md = MessageDigest.getInstance(digestAlgorithm)
		hashResource(md, res)
		Hexadecimal.fromByteArray(md.digest)
	}

	def hashDependency(md: MessageDigest, dep: Dependency)
	def hashResource(md: MessageDigest, res: Resource)
}

trait EvictionPolicy extends Cache{

	protected var thresholdSize: Long = EvictionPolicy.defaultThresholdSize
	final def setThresholdSize(size: Long){
		if(size < 0)
			warning("Could not set negative threshold size!")
		else if(size < evictedSize)
			warning("Could not set threshold size below evicted size!")
		else{
			val oldSize = thresholdSize
			thresholdSize = size
			if(oldSize < thresholdSize){ //lowering threshold => may need to evict something
				freeSpace
			}
		}
	}
	final def getThresholdSize: Long = thresholdSize
	
	protected var evictedSize: Long = EvictionPolicy.defaultEvictedSize
	final def setEvictedSize(size: Long){
		if(size < 0)
			warning("Could not set negative evicted size!")
		else if(size > thresholdSize)
			warning("Could not set evicted size above threshold size!")
		else
			evictedSize = size
	}
	final def getEvictedSize: Long = evictedSize

	//def compareDependencies(d1: Dependency, d2: Dependency): Int
	def evictionOrder(file: Iterable[File]): Stream[File]
}

object EvictionPolicy extends Reporter{
	val name: String = "CGP"
	private var defaultThresholdSize: Long = 100 << 20 //100MB
	private var defaultEvictedSize: Long = defaultThresholdSize >> 1 //50MB
	final def setDefaultThreshold(size: Long){
		if(size < 0)
			warning("Could not set negative threshold size!")
		else if(size < defaultEvictedSize)
			warning("Could not set threshold size below evicted size!")
		else{
			defaultThresholdSize = size
		}
	}

	final def getDefaultThreshold: Long = defaultThresholdSize

	final def setDefaultEvictedSize(size: Long){
		if(size < 0)
			warning("Could not set negative evicted size!")
		else if(size > defaultThresholdSize)
			warning("Could not set evicted size above threshold size!")
		else{
			defaultEvictedSize = size
		}
	}

	final def getEvictedSize: Long = defaultEvictedSize
}

//control cache existance
//this will be a class instance then (multiple concurrent executions)
object Cache extends Reporter{

	//import component.ExecutableComponent

	import scalab.caches.NoCache
	import java.io.{ByteArrayOutputStream, ObjectOutputStream, ByteArrayInputStream, ObjectInputStream}

	lazy val name = "Cache" 

	final def apply(cache: Cache) = setDefaultCache(cache)

	private var caches: List[Cache] = Nil
	final def getCaches: List[Cache] = caches

	private[core] var defaultCacheDir: String = "." + File.separator + ".scalab" + File.separator + "cache" + File.separator
	final def setDefaultCacheDir(dir: String){
		if(dir eq null)
			warning("Cannot set default cache directory to null! " +
				"Please use Cache.setDefaultCache(NoCache) if you want to disable default cache.")
		else
			defaultCacheDir = dir
	}
	final def getDefaultCacheDir: String = defaultCacheDir

	private[core] var defaultCache: Cache = NoCache()//new TimestampCache with SimpleCache
	final def setDefaultCache(cache: Cache){
		if(cache eq null)
			warning("Cannot set default cache to null! Please use the NoCache object if you want to disable default cache.")
		else
			defaultCache = cache
	}
	final def getDefaultCache: Cache = defaultCache

	final def createProductDirectory(task: Task): Unit = {
		import scalab.utils.FileUtils
		if(!FileUtils.mkDirs(task.productDirectory))
			task.fail("I could not create product directory '" + task.productDirectory + "'! you may not have sufficient privileges.")
	}

	//todo: add try/catch/finalize
	final def serialize(serializableObject: AnyRef): Array[byte] = {
		var byteArray: Array[Byte] = null
    val baos: ByteArrayOutputStream = new ByteArrayOutputStream
    val oos: ObjectOutputStream = new ObjectOutputStream(baos)
    oos.writeObject(serializableObject)
    oos.flush
    byteArray = baos.toByteArray
    oos.close
    baos.close
		byteArray
	}

	//todo: add try/catch/finalize
	final def deserialize[A >: Null](bytes: Array[byte]): A = {
		var obj: A = null
    val bais: ByteArrayInputStream = new ByteArrayInputStream(bytes)
   	val ois: ObjectInputStream = new ObjectInputStream(bais)
    obj = ois.readObject().asInstanceOf[A]
    ois.close
    bais.close
		obj
	}
}
